# -*- coding: utf-8 -*-
import win32pipe, win32file
from fpdf import FPDF
from datetime import datetime
from PIL import Image
import io
from os import path

PipeName = r'\\.\pipe\report2pdfpipe'
# send one payload in multi packages
RequestMaxLength = 1024
PackageMaxLength = 1024

# PipeCommunicationReturnCode
CodeSuccess = b'\x00'
CodeIncorrectRequest = b'\x01'
CodeIncorrectPayload = b'\x02'
CodePayloadComplete = b'\x03'
CodeFileIsSaved = b'\x04'
CodeServiceIsClosed = b'\x05'

# PipeCommunicationTypeEnum
RequestTypeSummary = 0
RequestTypeSetup = 1
RequestTypeOperation = 2
RequestTypeTeardown = 3
DataTypeText = 10
DataTypeImage = 11
DataTypeLink = 12

# page layout
PageMargin = 10
LineWidth = 180
LineHeight = 4
ImageMaxHeight = 100


class ReportToPDF(FPDF):
    def __init__(self):
        super().__init__()
        self.pipe_handle = None
        timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        self.filename = f"result{timestamp}.pdf"

    def __enter__(self):
        self.set_margins(left=PageMargin, top=PageMargin, right=PageMargin)
        self.add_font("SimSun", "", r"C:\Windows\Fonts\simsun.ttc", uni=True)
        self.set_text_color(0, 0, 0)  # set default text color is black
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Close the pipe
        if self.pipe_handle is not None:
            print(f"pipe is closing ...........")
            win32file.CloseHandle(self.pipe_handle)

    def run_service(self):
        self.pipe_handle = win32pipe.CreateNamedPipe(
            PipeName,
            win32pipe.PIPE_ACCESS_DUPLEX,
            win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
            1, 65536, 65536,
            0,
            None)

        # Wait for client to connect
        print(f"pipe is waiting ...")
        win32pipe.ConnectNamedPipe(self.pipe_handle, None)

        if self._receive_summary() is False:
            return

        if self._receive_all_operations() is False:
            return

        pdf_path = path.join(self.pdf_root_path, self.filename)
        # save pdf file
        self.output(pdf_path)

        # Close the pipe
        # print(f"pipe is closing...........")
        # win32file.CloseHandle(self.pipe_handle)

    def _receive_summary(self):
        # read summary request
        return_code, data_request = win32file.ReadFile(self.pipe_handle, RequestMaxLength)
        data_type = data_request[0]

        if data_type == RequestTypeSummary:
            win32file.WriteFile(self.pipe_handle, CodeSuccess)
        else:
            win32file.WriteFile(self.pipe_handle, CodeIncorrectRequest)
            print("receive summary, request type is incorrect.")
            return False

        # receive summary payload
        index = 1
        payload_length = int.from_bytes(data_request[index:index + 4], byteorder='little')  # 4 bytes of length
        index += 4
        self.setup_count = int.from_bytes(data_request[index:index + 4], byteorder='little')  # 4 bytes of length
        index += 4
        self.operation_count = int.from_bytes(data_request[index:index + 4], byteorder='little')  # 4 bytes of length
        index += 4
        self.teardown_count = int.from_bytes(data_request[index:index + 4], byteorder='little')  # 4 bytes of length
        index += 4

        self.pdf_root_path = data_request[index:].decode("utf-8")
        return_code, data_payload = win32file.ReadFile(self.pipe_handle, payload_length)
        if len(data_payload) == payload_length:
            win32file.WriteFile(self.pipe_handle, CodeSuccess)
        else:
            win32file.WriteFile(self.pipe_handle, CodeIncorrectPayload)
            print(f"receive summary request, expect {payload_length} bytes, received {len(data_payload)} bytes")
            return False

        # write summary to pdf
        self.add_page()
        data_length = len(data_payload)
        index = 0
        while index < data_length:
            line_type = data_payload[index]
            line_length_bytes = data_payload[index + 1:index + 5]  # 4 bytes
            line_bytes_length = int.from_bytes(line_length_bytes, byteorder='little')
            line_bytes = data_payload[index + 5:index + 5 + line_bytes_length]
            index = index + 5 + line_bytes_length

            if line_type == DataTypeText:
                line = line_bytes.decode("utf-8")
                self._write_line_text(line)
            else:
                print(f"receive summary payload, line type is incorrect.")
                return False

        return True

    def _receive_all_operations(self):
        is_received = True

        if self.setup_count > 0:
            is_received = self._receive_operations(RequestTypeSetup, "SETUP")
            if is_received is False:
                print("receive SETUP operations failed")
                return False

        if self.operation_count > 0:
            is_received = self._receive_operations(RequestTypeOperation, "OPERATIONS")
            if is_received is False:
                print("receive OPERATIONS operations failed")
                return False

        if self.teardown_count > 0:
            is_received = self._receive_operations(RequestTypeTeardown, "TEARDOWN")
            if is_received is False:
                print("receive TEARDOWN operations failed")
                return False

    def _receive_operations(self, operation_type, operations_title):
        # read request
        return_code, data_request = win32file.ReadFile(self.pipe_handle, RequestMaxLength)
        data_type = data_request[0]

        if data_type == operation_type:
            win32file.WriteFile(self.pipe_handle, CodeSuccess)
        else:
            win32file.WriteFile(self.pipe_handle, CodeIncorrectRequest)
            print(f"receive operation request, operation type is incorrect.")
            return False

        # add new page
        self.add_page()
        self._write_section_title(operations_title)

        operation_count = int.from_bytes(data_request[1:1 + 4], byteorder='little')  # 4 bytes of count
        for i in range(0, operation_count):
            # read request
            return_code, data_request = win32file.ReadFile(self.pipe_handle, RequestMaxLength)
            payload_type = data_request[0]
            if data_type == operation_type:
                win32file.WriteFile(self.pipe_handle, CodeSuccess)
            else:
                win32file.WriteFile(self.pipe_handle, CodeIncorrectRequest)
                print(f"receive operation request, operation type is incorrect.")
                return False

            index = 1
            payload_length = int.from_bytes(data_request[index:index + 4], byteorder='little')  # 4 bytes of length

            # read payload from client
            received_payload_bytes = bytes()
            while len(received_payload_bytes) < payload_length:
                return_code, payload_temp = win32file.ReadFile(self.pipe_handle, PackageMaxLength)
                # remove the first byte as it is index of payload
                received_payload_bytes = received_payload_bytes + payload_temp[1:]
                # respond payload
                win32file.WriteFile(self.pipe_handle, CodeSuccess)

            if len(received_payload_bytes) == payload_length:
                self._write_operation(received_payload_bytes)
            else:
                print(f"receive operation payload, expect {payload_length} bytes, received {len(received_payload_bytes)} bytes.")
                return False

        return True

    def _write_operation(self, data_bytes):
        data_length = len(data_bytes)
        index = 0

        while index < data_length:
            line_type = data_bytes[index]
            line_length_bytes = data_bytes[index + 1:index + 5]  # 4 bytes of length
            line_bytes_length = int.from_bytes(line_length_bytes, byteorder='little')
            line_bytes = data_bytes[index + 5:index + 5 + line_bytes_length]
            index = index + 5 + line_bytes_length

            if line_bytes_length > 0:
                if line_type == DataTypeText:
                    line = line_bytes.decode("utf-8")
                    self._write_line_text(line)
                elif line_type == DataTypeImage:
                    self._write_line_image(line_bytes)
                elif line_type == DataTypeLink:
                    line = line_bytes.decode("utf-8")
                    self._write_line_link(line)

        self._write_line_link("")

    def _write_section_title(self, text):
        self.set_font('Arial', style="B", size=15)
        self.multi_cell(LineWidth, LineHeight, text, 0, align='C')
        self.ln()

    def _write_line_text(self, text):
        self.set_font('SimSun', size=12)
        self.multi_cell(LineWidth, LineHeight, text, 0, align='L')
        self.ln()

    def _write_line_image(self, image_bytes):
        image = Image.open(io.BytesIO(image_bytes))  # create PIL image object

        with io.BytesIO() as img_mem_buffer:
            image.save(img_mem_buffer, format="PNG")  # save image object to memory buffer
            img_mem_buffer.seek(0)
            width, height = image.size
            # if image size is big, change to smaller size
            if width > LineWidth:
                ratio = LineWidth / width
                width *= ratio
                height *= ratio
            if height > ImageMaxHeight:
                ratio = ImageMaxHeight / height
                width *= ratio
                height *= ratio
            self.image(img_mem_buffer, w=width, h=height)
            self.ln()

    def _write_line_link(self, file_path):
        x = self.get_x()
        y = self.get_y()
        escaped_path = file_path.replace('\\', '/')
        link = f"file:///{escaped_path}"

        text_width = self.get_string_width(file_path)
        num_lines = int((text_width // LineWidth) + 1)
        # create link for every line
        for i in range(num_lines):
            self.link(x=x, y=y + i * LineHeight, w=LineWidth, h=LineHeight, link=link)

        # write text over link, so that the link is visible
        self.set_xy(x, y)
        self.set_text_color(0, 0, 255)  # set text color is blue
        self.set_font('SimSun', size=12)
        a = self.multi_cell(LineWidth, LineHeight, file_path, 0, align='L')
        self.set_text_color(0, 0, 0)  # set text color is black
        self.ln()
